﻿#region Using Directives

using System;
using System.Diagnostics;
using System.Reflection;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection.Emit;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using HttpClient = System.Net.WebClient;
using System.Threading;

#endregion

namespace Smugmug
{
    public class SmugmugClient
    {
        #region Fields

        private IWebClient client;
        private IProtocol protocol;
        private ILogger logger;
        string serviceUrl;
        string version;

        #endregion

        #region Constructor

        public SmugmugClient(IWebClient client, IProtocol protocol, ILogger logger)
            : this(Config.ServiceVersion, client, protocol, logger)
        {
        }

        public SmugmugClient(string version, IWebClient client, IProtocol protocol, ILogger logger)
        {
            this.version = version;
            this.client = client;
            this.protocol = protocol;
            this.logger = logger;
        }

        #endregion

        #region Methods

        private void MakeRequest(string method, bool secure, object arguments)
        {
            string resposneText = GetResponse(method, secure, UrlHelper.ObjectToDictionary(arguments));
            IResponse response = protocol.GetResponse(resposneText);
            if(response.Error != null)
                throw new SmugmugException((FaultCode)response.Error.ErrorCode);
        }

        private T MakeRequest<T>(string method, bool secure, object arguments)
            where T : new()
        {
            return MakeRequest<T>(method, secure, UrlHelper.ObjectToDictionary(arguments));
        }

        private T MakeRequest<T>(string method, bool secure, Dictionary<string, string> arguments)
             where T : new()
        {
            string resposneText = GetResponse(method, secure, arguments);
            IResponse<T> response = protocol.GetResponse<T>(resposneText);
            if (response.Error != null)
                throw new SmugmugException((FaultCode)response.Error.ErrorCode);
            return response.Data;
        }

        private string GetResponse(string method, bool secure, Dictionary<string, string> arguments)
        {
            ClearLastResponse();
            logger.Info(method);
#if DEBUG
            LastMethod = method;
#endif

            Uri url = UrlHelper.BuildUrl(method, secure, ServiceUrl, arguments);
            logger.Info(url.ToString());
#if DEBUG
            LastRequest = url;
#endif

            string response = client.MakeRequest(url);
            logger.Info(response);

#if DEBUG
            LastResponse = response;
#endif
            return response;
        }

        private void ClearLastResponse()
        {
#if DEBUG
            LastMethod = null;
            LastRequest = null;
            LastResponse = null;
#endif
        }

        #endregion

        #region Properties

        private string ServiceUrl
        {
            get
            {
                return String.Format(CultureInfo.CurrentCulture, "{0}/{1}/", protocol.BaseUrl, version);
            }
        }

        public IWebClient WebClient
        {
            get { return client; }
            set { client = value; }
        }

        public IProtocol Protocol
        {
            get { return protocol; }
            set { protocol = value; serviceUrl = protocol.BaseUrl; }
        }

        public ILogger Logger
        {
            get { return logger; }
            set { logger = value; }
        }


#if DEBUG
        public Uri LastRequest { get; set; }
        public string LastResponse { get; set; }
        public string LastMethod { get; set; }
#endif
        #endregion

        #region API Methods

        #region Albums

        //TODO: test this method
        public void ApplyAlbumWatermark(string sessionId, int albumId, string callback, bool? pretty, bool? strict, int watermarkId)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.albums.applyWatermark", false, new
            {
                SessionID = sessionId,
                AlbumID = albumId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict,
                WatermarkID = watermarkId
            });
        }

        public AlbumCollection GetAlbums(string sessionId, string callback, string extras, bool? heavy, int? lastUpdated, string nickName, bool? pretty, string sitePassword, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<AlbumCollection>("smugmug.albums.get", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                Extras = extras,
                Heavy = heavy,
                LastUpdated = lastUpdated,
                NickName = nickName,
                Pretty = pretty,
                SitePassword = sitePassword,
                Strict = strict
            });
        }

        public Album GetAlbumInfo(string sessionId, int albumId, string albumKey, string callback, string password, bool? pretty, string sitePassword, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(albumKey, "albumKey");

            return MakeRequest<Album>("smugmug.albums.getInfo", false, new
            {
                SessionID = sessionId,
                AlbumID = albumId,
                AlbumKey = albumKey,
                Callback = callback,
                Password = password,
                Pretty = pretty,
                SitePassword = sitePassword,
                Strict = strict
            });
        }

        public Album CreateAlbum(string sessionId, Album album, string callback, bool? pretty, bool? strict, bool? unique)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNull(album, "album");
            Require.NotNullOrEmpty(album.Title, "album.Title");

            return MakeRequest<Album>("smugmug.albums.create", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                Title = album.Title,
                Backprinting = album.Backprinting,
                CanRank = album.CanRank,
                CategoryID = (int?)(album.Category == null ? null : new int?(album.Category.Id)),
                Clean = album.Clean,
                Comments = album.Comments,
                CommunityID = (int?)(album.Community == null ? null : new int?(album.Community.Id)),
                DefaultColor = album.DefaultColor,
                EXIF = album.Exif,
                External = album.External,
                FamilyEdit = album.FamilyEdit,
                Filenames = album.Filenames,
                FriendEdit = album.FriendEdit,
                Geography = album.Geography,
                Header = album.Header,
                HideOwner = album.HideOwner,
                Larges = album.Larges,
                Originals = album.Originals,
                Password = album.Password,
                PasswordHint = album.PasswordHint,
                Printable = album.Printable,
                ProofDays = album.ProofDays,
                Protected = album.Protected,
                Public = album.Public,
                Share = album.Share,
                SmugSearchable = album.SmugSearchable,
                SortDirection = album.SortDirection,
                SortMethod = album.SortMethod,
                SquareThumbs = album.SquareThumbs,
                SubCategoryID = (int?)(album.SubCategory == null ? null : new int?(album.SubCategory.Id)),
                TemplateID = (int?)(album.Template == null ? null : new int?(album.Template.Id)),
                ThemeID = (int?)(album.Theme == null ? null : new int?(album.Theme.Id)),
                UnsharpAmount = album.UnsharpAmount,
                UnsharpRadius = album.UnsharpRadius,
                UnsharpSigma = album.UnsharpSigma,
                UnsharpThreshold = album.UnsharpThreshold,
                WatermarkID = (int?)(album.Watermark == null ? null : new int?(album.Watermark.Id)),
                Watermarking = album.Watermarking,
                WorldSearchable = album.WorldSearchable,
                X2Larges = album.X2Larges,
                X3Larges = album.X3Larges,
                XLarges = album.XLarges,
                Pretty = pretty,
                Strict = strict,
                Unique = unique
            });
        }

        public void DeleteAlbum(string sessionId, int albumId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.albums.delete", false, new
            {
                SessionID = sessionId,
                AlbumID = albumId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public AlbumStats GetAlbumStats(string sessionId, int albumId, int month, int year, bool? heavy)
        {
            throw new NotImplementedException("This method is not yet supported by the Smugmug service");

            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<AlbumStats>("smugmug.albums.getStats", false, new
            {
                SessionID = sessionId,
                AlbumID = albumId,
                Month = month,
                Year = year,
                Heavy = heavy
            });
        }

        //TODO: test this method
        public void RemoveAlbumWatermark(string sessionId, int albumId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.albums.removeWatermark", false, new
            {
                SessionID = sessionId,
                AlbumID = albumId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: test this method
        public void ReSortAlbum(string sessionId, int albumId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.albums.reSort", false, new
            {
                SessionID = sessionId,
                AlbumID = albumId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public void ChangeAlbumSetting<T>(string sessionId, int albumId, Expression<Func<Album, T>> property, T value, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            var expression = (MemberExpression)property.Body;
            var arguments = UrlHelper.ObjectToDictionary(new
            {
                SessionID = sessionId,
                AlbumID = albumId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
            arguments[expression.Member.Name] = value.ToString();
            MakeRequest<Album>("smugmug.albums.changeSettings", false, arguments);
        }

        //TODO: test this method
        public void ChangeAlbumSettings(string sessionId, Album album, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            var arguments = UrlHelper.ObjectToDictionary(new
            {
                album
            });
            arguments["SessionID"] = sessionId;
            arguments["Callback"] = callback;
            arguments["Pretty"] = pretty.HasValue ? pretty.Value.ToString() : "";
            arguments["Strict"] = strict.HasValue ? strict.Value.ToString() : "";
            MakeRequest<Album>("smugmug.albums.changeSettings", false, arguments);
        }

        private int? EnumToInt<T>(Nullable<T> val)
            where T : struct
        {
            if (!val.HasValue)
                return null;
            return new Nullable<int>(Convert.ToInt32(val.Value));
        }

        #endregion

        #region Album Templates

        //TODO: Test this method
        public AlbumTemplateCollection GetAlbumTemplates(string sessionId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<AlbumTemplateCollection>("smugmug.albumtemplates.get", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public AlbumTemplate CreateAlbumTemplate(string sessionId, AlbumTemplate template, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNull(template, "template");
            Require.NotNullOrEmpty(template.AlbumTemplateName, "template.AlbumTemplateName");

            Dictionary<string, string> args = UrlHelper.ObjectToDictionary(template);
            args["SessionID"] = sessionId;
            args["Callback"] = callback;
            args["Pretty"] = pretty.HasValue ? pretty.Value.ToString() : "";
            args["Strict"] = strict.HasValue ? strict.Value.ToString() : "";

            return MakeRequest<AlbumTemplate>("smugmug.albumtemplates.create", false, args);
        }

        //TODO: Test this method
        public void DeleteAlbumTemplate(string sessionId, int albumTemplateId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            MakeRequest("smugmug.albumtemplates.delete", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                AlbumTemplateID = albumTemplateId,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public void ChangeAlbumTemplateSettings(string sessionId, AlbumTemplate template, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNull(template, "template");

            Dictionary<string, string> args = UrlHelper.ObjectToDictionary(template);
            args["SessionID"] = sessionId;
            args["Callback"] = callback;
            args["Pretty"] = pretty.HasValue ? pretty.Value.ToString() : "";
            args["Strict"] = strict.HasValue ? strict.Value.ToString() : "";

            MakeRequest<AlbumTemplate>("smugmug.albumtemplates.changeSettings", false, args);
        }

        //TODO: Test this method
        public void ChangeAlbumTemplateSetting<T>(string sessionId, int albumTemplateId, Expression<Func<Album, T>> property, T value, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            var expression = (MemberExpression)property.Body;
            var args = UrlHelper.ObjectToDictionary(new { SessionID = sessionId, AlbumTemplateID = albumTemplateId });
            args[expression.Member.Name] = value.ToString();
            args["Callback"] = callback;
            args["Pretty"] = pretty.HasValue ? pretty.Value.ToString() : "";
            args["Strict"] = strict.HasValue ? strict.Value.ToString() : "";
            MakeRequest<AlbumTemplate>("smugmug.albumtemplates.changeSettings", false, args);
        }

        #endregion

        #region Categories

        //TODO: Test this method
        public Category CreateCategory(string sessionId, string name, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(name, "name");

            return MakeRequest<Category>("smugmug.categories.create", false, new
            {
                SessionID = sessionId,
                Name = name,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public void DeleteCategory(string sessionId, int categoryId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.categories.delete", false, new
            {
                SessionID = sessionId,
                CategoryID = categoryId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public CategoryCollection GetCategories(string sessionId, bool? nickname, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<CategoryCollection>("smugmug.categories.get", false, new
            {
                SessionID = sessionId,
                Nickname = nickname,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public void RenameCatgory(string sessionId, int categoryId, string name, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(name, "name");

            MakeRequest("smugmug.categories.rename", false, new
            {
                SessionID = sessionId,
                CategoryID = categoryId,
                Name = name,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Communities

        public CommunityCollection GetCommunities(string sessionId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<CommunityCollection>("smugmug.communities.get", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Family

        //TODO: Test this method
        public void AddFamily(string sessionId, string nickname, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(nickname, "nickname");

            MakeRequest("smugmug.family.add", false, new
            {
                SessionID = sessionId,
                Nickname = nickname,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public FamilyCollection GetFamily(string sessionId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<FamilyCollection>("smugmug.family.get", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public void RemoveFamily(string sessionId, string nickname, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(nickname, "nickname");

            MakeRequest("smugmug.family.remove", false, new
            {
                SessionID = sessionId,
                Nickname = nickname,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public void RemoveAllFamily(string sessionId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.family.removeAll", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Friends

        //TODO: Test this method
        public void AddFriend(string sessionId, string nickname, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(nickname, "nickname");

            MakeRequest("smugmug.friends.add", false, new
            {
                SessionID = sessionId,
                Nickname = nickname,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public FriendCollection GetFriends(string sessionId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<FriendCollection>("smugmug.friends.get", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public void RemoveFriend(string sessionId, string nickname, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(nickname, "nickname");

            MakeRequest("smugmug.friends.remove", false, new
            {
                SessionID = sessionId,
                Nickname = nickname,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public void RemoveAllFriends(string sessionId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.friends.removeAll", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Images

        //TODO: test this method
        public void ApplyImageWatermark(string sessionId, int imageId, string callback, bool? pretty, bool? strict, int watermarkId)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.images.applyWatermark", false, new
            {
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: test this method
        public void ChangeImagePosition(string sessionId, int imageId, int position, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.images.changePosition", false, new
            {
                SessionID = sessionId,
                ImageID = imageId,
                Position = position,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public void ChangeImageSetting<T>(string sessionId, int imageId, Expression<Func<Album, T>> property, T value, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            var expression = (MemberExpression)property.Body;
            var arguments = UrlHelper.ObjectToDictionary(new
            {
                SessionID = sessionId,
                ImageID = imageId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
            arguments[expression.Member.Name] = value.ToString();
            MakeRequest<Album>("smugmug.images.changeSettings", false, arguments);
        }

        public void CropImage(string sessionId, bool? pretty, bool? strict)
        {
            throw new NotSupportedException("This method is not supported yet.");
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.images.crop", false, new
            {
                SessionID = sessionId,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: test this method and check parameters
        public void DeleteImage(string sessionId, int imageId, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.images.delete", false, new
            {
                SessionID = sessionId,
                ImageID = imageId,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: test this method and check parameters
        public Image GetImageExif(string sessionId, int imageId, string imageKey, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<Image>("smugmug.images.getEXIF", false, new
            {
                SessionID = sessionId,
                ImageID = imageId,
                ImageKey = imageKey,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: test this method and check parameters
        public Image GetImageInfo(string sessionId, int imageId, string imageKey, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<Image>("smugmug.images.getInfo", false, new
            {
                SessionID = sessionId,
                ImageID = imageId,
                ImageKey = imageKey,
                Pretty = pretty,
                Strict = strict
            });
        }

        public ImageStats GetImageStats(string sessionId, int imageId, int? month, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<ImageStats>("smugmug.images.getStats", false, new
            {
                SessionID = sessionId,
                Pretty = pretty,
                Strict = strict
            });
        }

        public void GetImagePricing(string sessionId, bool? pretty, bool? strict)
        {
            throw new NotSupportedException("This method is not supported yet.");
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.images.pricing.get", false, new
            {
                SessionID = sessionId,
                Pretty = pretty,
                Strict = strict
            });
        }

        //
        public void RotateImage(string sessionId, bool? pretty, bool? strict)
        {
            throw new NotSupportedException("This method is not supported yet.");
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.images.rotate", false, new
            {
                SessionID = sessionId,
                Pretty = pretty,
                Strict = strict
            });
        }

        private UploadContext GetUploadContext(string sessionId, Stream stream, string fileName, int albumId, string albumKey, string caption, string keywords)
        {
            UploadContext context = new UploadContext();
            context.Request.KeepAlive = false;
            context.Request.Timeout = -1;
            context.Request.ReadWriteTimeout = -1;
            context.Request.Headers.Add("X-Smug-SessionID", sessionId);
            context.Request.Headers.Add("X-Smug-Version", Config.ServiceVersion);
            context.Request.Headers.Add("X-Smug-ResponseType", protocol.ResponseType);
            context.Request.Headers.Add("X-Smug-AlbumID", albumId.ToString());
            context.Request.Headers.Add("X-Smug-AlbumKey", albumKey);
            context.Request.Headers.Add("X-Smug-FileName", fileName);
            if (!String.IsNullOrEmpty(caption))
                context.Request.Headers.Add("X-Smug-Caption", caption);
            if (!String.IsNullOrEmpty(keywords))
                context.Request.Headers.Add("X-Smug-Keywords", keywords);
            //TODO: Add ImageId parameter so images can be replaced
            //if (imageId.HasValue)
            //    context.Headers.Add("X-Smug-ImageID", imageId.Value.ToString());

            //TODO: Add latitude parameter
            //if (latitude.HasValue)
            //    context.Headers.Add("X-Smug-Latitude", latitude.Value.ToString());

            //TODO: Add longitude parameter
            //if (longitude.HasValue)
            //    context.Headers.Add("X-Smug-Longitude", longitude.Value.ToString());

            //TODO: Add altitude parameter
            //if (altitude.HasValue)
            //    context.Headers.Add("X-Smug-Latitude", altitude.Value.ToString());

            context.Request.ContentType = "image/jpeg";
            context.Request.UserAgent = Config.UserAgent;
            context.PhotoStream = stream;
            context.Request.ContentLength = context.PhotoStream.Length;

            // This option prevents uploads from being buffered into memory, avoiding OutOfMemory exceptions on large uploads.
            context.Request.AllowWriteStreamBuffering = false;

            context.RequestStream = context.Request.GetRequestStream();
            context.ChunkSize = Math.Max((int)(context.PhotoStream.Length / 100), 65536);

            return context;
        }

        private bool UploadChunk(UploadContext context)
        {
            if (context.CurrentPosition < context.PhotoStream.Length)
            {
                int chunk = Math.Min(context.ChunkSize, (int)(context.PhotoStream.Length - context.CurrentPosition));

                byte[] dataBuffer = new byte[chunk];
                int offset = 0;
                int toRead = chunk; 
                while (toRead > 0)
                {
                    int read = context.PhotoStream.Read(dataBuffer, offset, toRead);
                    if (read <= 0)
                    {
                        throw new EndOfStreamException(
                            String.Format("End of file reached with {0} bytes left to read", toRead));
                    }
                    toRead -= read;
                    offset += read;
                }

                context.RequestStream.Write(dataBuffer, 0, chunk);
                context.CurrentPosition += chunk;
            }

            return context.CurrentPosition < context.PhotoStream.Length;
        }

        public Image UploadImage(string sessionId, string path, int albumId, string albumKey, string caption, string keywords, IUploadProgress callback)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            FileInfo info = new FileInfo(path);
            if (!info.Exists)
            {
                throw new ArgumentException("Image does not exist: " + info.FullName);
            }
            FileStream stream = File.OpenRead(info.FullName);
            UploadContext context = GetUploadContext(sessionId, stream, path, albumId, albumKey, caption, keywords);
            Logger.Info("Uploading {0} ({1} bytes)", path, stream.Length);
            while (UploadChunk(context))
            {
                Logger.Info("{0}% complete", context.PercentComplete);
                if (callback != null)
                {
                    callback.SetProgress(context.PercentComplete);

                    if (callback.Canceled)
                    {
                        Logger.Warn("Upload Cancelled");
                        return null;
                    }
                }
            }
            Logger.Info("Upload Complete");

            context.RequestStream.Close();
            WebResponse httpResponse = context.Request.GetResponse();
            StreamReader reader = new StreamReader(httpResponse.GetResponseStream());
            string result = reader.ReadToEnd();
            reader.Close();
            httpResponse.Close();

            Logger.Info(result);

            var response = protocol.GetResponse<Image>(result);
            if (response.Error != null)
            {
                SmugmugException exception = new SmugmugException((FaultCode)response.Error.ErrorCode);
                if (callback != null)
                    callback.Error(exception);
                else
                    throw exception;
            }
            if (callback != null)
                callback.Completed(response.Data);
            return response.Data;
        }

        //
        public Image UploadImageFromUrl(string sessionId, int albumId, string caption, string url, string keywords, string latitude, string longitude, string altitude, int? byteCount, string md5Sum, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(url, "url");
            Require.NotNullOrEmpty(caption, "caption");

            return MakeRequest<Image>("smugmug.images.uploadFromURL", false, new
            {
                SessionID = sessionId,
                AlbumID = albumId,
                Caption = caption,
                URL = url,
                Keywords = keywords,
                Latitude = latitude,
                Longitude = longitude,
                Altitude = altitude,
                ByteCount = byteCount,
                MD5Sum = md5Sum,
                Pretty = pretty,
                Strict = strict
            });
        }

        //
        public void ZoomImageThumbnail(string sessionId, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.images.applyWatermark", false, new
            {
                SessionID = sessionId,
                Pretty = pretty,
                Strict = strict
            });
        }

        public ImageCollection GetImages(string sessionId, int albumId, bool heavy, string password, string sitePassword, string albumKey)
        {
            Album album = MakeRequest<Album>("smugmug.images.get", false, new
            {
                SessionID = sessionId,
                AlbumID = albumId,
                Heavy = heavy,
                Password = password,
                SitePassword = sitePassword,
                AlbumKey = albumKey
            });
            return album.Images;
        }

        public Image GetImageUrls(string sessionId, int imageId, TemplateType templateId, string password, string sitePassword, string imageKey)
        {
            return MakeRequest<Image>("smugmug.images.getURLs", false, new
            {
                SessionID = sessionId,
                ImageID = imageId,
                TemplateID = templateId,
                Password = password,
                SitePassword = sitePassword,
                ImageKey = imageKey
            });
        }

        //TODO: test this method
        public void RemoveImageWatermark(string sessionId, int imageId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.images.removeWatermark", false, new
            {
                SessionID = sessionId,
                ImageID = imageId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Login

        public Login LoginAnonymously(string apiKey, string callback, bool? pretty, bool? strict)
        {
            return MakeRequest<Login>("smugmug.login.anonymously", false, new
            {
                APIKey = apiKey,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public Login LoginWithHash(int userId, string passwordHash, string apiKey, string callback, bool? pretty, bool? strict)
        {
            return MakeRequest<Login>("smugmug.login.withHash", true, new
            {
                UserID = userId,
                PasswordHash = passwordHash,
                APIKey = apiKey,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public Login LoginWithPassword(string apiKey, string emailAddress, string password, string callback, bool? pretty, bool? strict)
        {
            return MakeRequest<Login>("smugmug.login.withPassword", true, new
            {
                APIKey = apiKey,
                EmailAddress = emailAddress,
                Password = password,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Logout

        public void Logout(string sessionId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.logout", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Products

        public ProductCollection GetProducts(string sessionId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<ProductCollection>("smugmug.products.get", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Pro Pricing

        public ProductCollection GetAlbumProPricing(string sessionId, int albumId, int? productId, string productType, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<ProductCollection>("smugmug.propricing.getAlbum", false, new
            {
                SessionID = sessionId,
                AlbumID = albumId,
                ProductID = productId,
                ProductType = productType,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public ProductCollection GetImageProPricing(string sessionId, int imageId, int? productId, string productType, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<ProductCollection>("smugmug.propricing.getImage", false, new
            {
                SessionID = sessionId,
                ImageID = imageId,
                ProductID = productId,
                ProductType = productType,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public ProductCollection GetPortfollioProPricing(string sessionId, int? productId, string productType, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<ProductCollection>("smugmug.propricing.getPortfolio", false, new
            {
                SessionID = sessionId,
                ProductID = productId,
                ProductType = productType,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public void SetAlbumProPricing(string sessionId, int albumId, float price, int? productId, string productType, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.propricing.setAlbum", false, new
            {
                SessionID = sessionId,
                AlbumID = albumId,
                Price = price,
                ProductID = productId,
                ProductType = productType,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public void SetImageProPricing(string sessionId, int imageId, float price, int? productId, string productType, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.propricing.setImage", false, new
            {
                SessionID = sessionId,
                ImageID = imageId,
                Price = price,
                ProductID = productId,
                ProductType = productType,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public void SetAlbumProPricing(string sessionId, float price, int? productId, string productType, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.propricing.setPortfolio", false, new
            {
                SessionID = sessionId,
                Price = price,
                ProductID = productId,
                ProductType = productType,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Share Groups

        public void AddShareGroupAlbum(string sessionId, int shareGroupId, int albumId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.sharegroups.addAlbum", false, new
            {
                SessionID = sessionId,
                ShareGroupID = shareGroupId,
                AlbumID = albumId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public void RemoveShareGroupAlbum(string sessionId, int shareGroupId, int albumId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.sharegroups.removeAlbum", false, new
            {
                SessionID = sessionId,
                ShareGroupID = shareGroupId,
                AlbumID = albumId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public ShareGroup CreateShareGroup(string sessionId, string name, string description, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(name, "name");

            return MakeRequest<ShareGroup>("smugmug.sharegroups.create", false, new
            {
                SessionID = sessionId,
                Name = name,
                Description = description,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public void DeleteShareGroup(string sessionId, int shareGroupId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.sharegroups.delete", false, new
            {
                SessionID = sessionId,
                ShareGroupID = shareGroupId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public AlbumCollection GetShareGroupAlbums(string sessionId, string shareGroupTag, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<AlbumCollection>("smugmug.sharegroups.getAlbums", false, new
            {
                SessionID = sessionId,
                ShareGroupTag = shareGroupTag,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public ShareGroup GetShareGroupInfo(string sessionId, int shareGroupId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<ShareGroup>("smugmug.sharegroups.getInfo", false, new
            {
                SessionID = sessionId,
                ShareGroupID = shareGroupId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public ShareGroupCollection GetShareGroups(string sessionId, bool? heavy, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<ShareGroupCollection>("smugmug.sharegroups.get", false, new
            {
                SessionID = sessionId,
                Heavy = heavy,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Styles

        public TemplateCollection GetStyles(string callback, bool? pretty, bool? strict)
        {
            return MakeRequest<TemplateCollection>("smugmug.styles.get", false, new
            {
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Sub Categories

        //TODO: Test this method
        public Category CreateSubCategory(string sessionId, string name, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(name, "name");

            return MakeRequest<Category>("smugmug.subcategories.create", false, new
            {
                SessionID = sessionId,
                Name = name,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public void DeleteSubCategory(string sessionId, int categoryId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.subcategories.delete", false, new
            {
                SessionID = sessionId,
                CategoryID = categoryId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public CategoryCollection GetSubCategories(string sessionId, bool? nickname, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<CategoryCollection>("smugmug.subcategories.get", false, new
            {
                SessionID = sessionId,
                Nickname = nickname,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        //TODO: Test this method
        public void RenameSubCatgory(string sessionId, int categoryId, string name, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(name, "name");

            MakeRequest("smugmug.subcategories.rename", false, new
            {
                SessionID = sessionId,
                CategoryID = categoryId,
                Name = name,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Themes

        public ThemeCollection GetThemes(string sessionId, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<ThemeCollection>("smugmug.styles.get", false, new
            {
                SessionID = sessionId,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #region Users

        public User GetUserDisplayName(string sessionId, string nickname, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");
            Require.NotNullOrEmpty(nickname, "nickname");

            return MakeRequest<User>("smugmug.users.getDisplayName", false, new
            {
                SessionID = sessionId,
                Nickname = nickname,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public UserTree GetUserTree(string sessionId, string nickname, bool? heavy, int? lastUpdated, string sitePassword, string callback, bool? pretty, bool? strict)
        {
            Require.NotNullOrEmpty(sessionId, "sessionId");

            return MakeRequest<UserTree>("smugmug.users.getTree", false, new
            {
                SessionID = sessionId,
                Nickname = nickname,
                Heavy = heavy,
                LastUpdated = lastUpdated,
                SitePassword = sitePassword,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        public void GetUserTransferStats(string sessionId, int month, int year, string callback, bool? pretty, bool? strict)
        {
            throw new NotImplementedException("Method not supported by Smugmug API yet");
            Require.NotNullOrEmpty(sessionId, "sessionId");

            MakeRequest("smugmug.users.getTransferStats", false, new
            {
                SessionID = sessionId,
                Month = month,
                Year = year,
                Callback = callback,
                Pretty = pretty,
                Strict = strict
            });
        }

        #endregion

        #endregion
    }
}